package com.tuuzed.hydrogen;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;

import com.tuuzed.hydrogen.internal.HydrogenContext;
import com.tuuzed.hydrogen.internal.ModelCursorProcessor;
import com.tuuzed.hydrogen.internal.Property;
import com.tuuzed.hydrogen.internal.SQLiteHelper;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

public class Hydrogen {
    private static final String TAG = "Hydrogen";

    public static <T extends Model> boolean save(@NonNull T model) {
        long count = count(model.getClass(), false, "_id = ?", new String[]{String.valueOf(model.getId())});
        return count <= 0 ? insert(model) : update(model);
    }

    public static <T extends Model> boolean insert(@NonNull T model) {
        String tableName = getTableName(model.getClass());
        SQLiteDatabase database = getWritableDatabase();
        ContentValues values = getContentValues(model);
        return SQLiteHelper.insert(database, tableName, null, values) != -1;
    }

    public static <T extends Model> void insertOrThrow(@NonNull T model) throws SQLException {
        String tableName = getTableName(model.getClass());
        SQLiteDatabase database = getWritableDatabase();
        ContentValues values = getContentValues(model);
        SQLiteHelper.insertOrThrow(database, tableName, null, values);
    }

    public static <T extends Model> long insertTx(@NonNull Collection<T> models) {
        return insertTx(models, true);
    }

    public static <T extends Model> long insertTx(@NonNull Collection<T> models, boolean errorContinue) {
        SQLiteDatabase database = getWritableDatabase();
        long count = 0;
        try {
            database.beginTransaction();
            for (T model : models) {
                boolean isSuccess = insert(model);
                if (isSuccess) {
                    count++;
                } else if (!errorContinue) {
                    return -1;
                }
            }
            if (count > 0) database.setTransactionSuccessful();
        } finally {
            database.endTransaction();
        }
        return count;
    }

    public static <T extends Model> boolean update(@NonNull T model) {
        try {
            updateOrThrow(model);
            return true;
        } catch (SQLException e) {
            Log.e(TAG, "update: " + model, e);
            return false;
        }
    }

    public static <T extends Model> void updateOrThrow(@NonNull T model) throws SQLException {
        SQLiteDatabase database = getWritableDatabase();
        String table = getTableName(model.getClass());
        ContentValues values = getContentValues(model);
        String[] whereArgs = {String.valueOf(model.getId())};
        SQLiteHelper.updateOrThrow(database, table, values, "_id = ?", whereArgs);
    }

    public static <T extends Model> long updateTx(@NonNull Collection<T> models) {
        return updateTx(models, true);
    }

    public static <T extends Model> long updateTx(@NonNull Collection<T> models, boolean errorContinue) {
        SQLiteDatabase database = getWritableDatabase();
        long count = 0;
        try {
            database.beginTransaction();
            for (T model : models) {
                boolean isSuccess = update(model);
                if (isSuccess) {
                    count++;
                } else if (!errorContinue) {
                    return -1;
                }
            }
            if (count > 0) database.setTransactionSuccessful();
        } finally {
            database.endTransaction();
        }
        return count;
    }


    public static <T extends Model> boolean delete(@NonNull T model) {
        String[] whereArgs = {String.valueOf(model.getId())};
        return delete(model.getClass(), "_id = ?", whereArgs) > 0;
    }

    public static <T extends Model> int deleteAll(@NonNull Class<T> clazz) {
        return delete(clazz, null, null);
    }


    public static <T extends Model> int delete(@NonNull Class<T> clazz,
                                               @Nullable String whereClause,
                                               @Nullable String[] whereArgs) {
        String tableName = getTableName(clazz);
        SQLiteDatabase database = getWritableDatabase();
        return SQLiteHelper.delete(database, tableName, whereClause, whereArgs);
    }

    @Nullable
    public static <T extends Model> T findById(@NonNull Class<T> clazz, long id) {
        return findById(clazz, id, null, null, null);
    }

    @Nullable
    public static <T extends Model> T findById(@NonNull Class<T> clazz, long id,
                                               @Nullable String[] columns,
                                               @Nullable String whereClause,
                                               @Nullable String[] whereArgs) {
        if (TextUtils.isEmpty(whereClause)) {
            whereClause = "_id = ?";
        } else {
            whereClause = "_id = ? AND (" + whereClause + ")";
        }
        if (whereArgs == null) {
            whereArgs = new String[]{String.valueOf(id)};
        } else {
            String[] tmp = new String[whereArgs.length + 1];
            tmp[0] = String.valueOf(0);
            System.arraycopy(whereArgs, 0, tmp, 1, whereArgs.length);
            whereArgs = tmp;
        }
        Iterator<T> iterator = null;
        try {
            iterator = findAsIterator(clazz, false, columns, whereClause, whereArgs, null, null, null, "0,1");
            if (iterator.hasNext()) {
                return iterator.next();
            }
        } finally {
            if (iterator != null) {
                iterator.remove();
            }
        }
        return null;
    }

    @NonNull
    public static <T extends Model> List<T> findAll(@NonNull Class<T> clazz) {
        return findAll(clazz, null);
    }

    @NonNull
    public static <T extends Model> List<T> findAll(@NonNull Class<T> clazz, @Nullable String orderBy) {
        return findAll(clazz, null, null, orderBy);
    }

    @NonNull
    public static <T extends Model> List<T> findAll(@NonNull Class<T> clazz,
                                                    @Nullable String whereClause,
                                                    @Nullable String[] whereArgs) {
        return findAll(clazz, whereClause, whereArgs, null);
    }

    @NonNull
    public static <T extends Model> List<T> findAll(@NonNull Class<T> clazz,
                                                    @Nullable String whereClause,
                                                    @Nullable String[] whereArgs,
                                                    @Nullable String orderBy) {
        return findAll(clazz, false, null, whereClause, whereArgs, null, null, orderBy, null);
    }

    @NonNull
    public static <T extends Model> List<T> findAll(@NonNull Class<T> clazz,
                                                    boolean distinct,
                                                    @Nullable String[] columns,
                                                    @Nullable String whereClause,
                                                    @Nullable String[] whereArgs,
                                                    @Nullable String groupBy,
                                                    @Nullable String having,
                                                    @Nullable String orderBy,
                                                    @Nullable String limit) {
        List<T> list = new ArrayList<>();
        Iterator<T> iterator = null;
        try {
            iterator = findAsIterator(clazz, distinct, columns, whereClause, whereArgs, groupBy, having, orderBy, limit);
            while (iterator.hasNext()) {
                T next = iterator.next();
                list.add(next);
            }
        } finally {
            if (iterator != null) {
                iterator.remove();
            }
        }
        return list;
    }

    @NonNull
    public static <T extends Model> Iterator<T> findAsIterator(@NonNull Class<T> clazz) {
        return findAsIterator(clazz, null);
    }

    @NonNull
    public static <T extends Model> Iterator<T> findAsIterator(@NonNull Class<T> clazz,
                                                               @Nullable String orderBy) {
        return findAsIterator(clazz, null, null, orderBy);
    }

    @NonNull
    public static <T extends Model> Iterator<T> findAsIterator(@NonNull Class<T> clazz,
                                                               @Nullable String whereClause,
                                                               @Nullable String[] whereArgs) {
        return findAsIterator(clazz, whereClause, whereArgs, null);
    }

    @NonNull
    public static <T extends Model> Iterator<T> findAsIterator(@NonNull Class<T> clazz,
                                                               @Nullable String whereClause,
                                                               @Nullable String[] whereArgs,
                                                               @Nullable String orderBy) {
        return findAsIterator(clazz, false, null, whereClause, whereArgs, null, null, orderBy, null);
    }

    @NonNull
    public static <T extends Model> Iterator<T> findAsIterator(@NonNull Class<T> clazz,
                                                               boolean distinct,
                                                               @Nullable String[] columns,
                                                               @Nullable String whereClause,
                                                               @Nullable String[] whereArgs,
                                                               @Nullable String groupBy,
                                                               @Nullable String having,
                                                               @Nullable String orderBy,
                                                               @Nullable String limit) {
        String tableName = getTableName(clazz);
        SQLiteDatabase database = getReadableDatabase();
        Cursor cursor = SQLiteHelper.query(database, distinct, tableName, columns, whereClause,
                whereArgs, groupBy, having, orderBy, limit);
        return ModelCursorProcessor.create(cursor, clazz);
    }

    public static long count(@NonNull Class<? extends Model> clazz) {
        return count(clazz, false, null, null);
    }

    public static long count(@NonNull Class<? extends Model> clazz,
                             boolean distinct,
                             @Nullable String whereClause,
                             @Nullable String[] whereArgs) {
        String tableName = getTableName(clazz);
        SQLiteDatabase database = getReadableDatabase();
        String[] columns = {"COUNT(_id)"};
        Cursor cursor = null;
        long count = 0;
        try {
            cursor = SQLiteHelper.query(database, distinct, tableName, columns, whereClause,
                    whereArgs, null, null, null, null);
            if (cursor.moveToFirst()) {
                count = cursor.getLong(0);
            }
            return count;
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }


    @NonNull
    private static <T extends Model> ContentValues getContentValues(T model) {
        Class<? extends Model> clazz = model.getClass();
        List<Property> properties = getProperties(clazz);
        ContentValues values = new ContentValues(properties.size());
        for (Property property : properties) {
            putContentValue(values, property, model);
        }
        return values;
    }

    private static <T extends Model> void putContentValue(ContentValues values,
                                                          Property property, T model) {
        Object obj = null;
        try {
            obj = property.getValue(model);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        if (obj != null) {
            Class<?> type = property.getJavaType();
            if (obj instanceof String) {
                values.put(property.getColumnName(), (String) obj);
            } else if (obj instanceof Date) {
                values.put(property.getColumnName(), ((Date) obj).getTime());
            } else if (type == boolean.class || obj instanceof Boolean) {
                values.put(property.getColumnName(), (boolean) obj ? 1 : 0);
            } else if (type == byte.class || obj instanceof Byte) {
                values.put(property.getColumnName(), (byte) obj);
            } else if (type == char.class || obj instanceof Character) {
                values.put(property.getColumnName(), String.valueOf((char) obj));
            } else if (type == short.class || obj instanceof Short) {
                values.put(property.getColumnName(), (short) obj);
            } else if (type == int.class || obj instanceof Integer) {
                values.put(property.getColumnName(), (int) obj);
            } else if (type == long.class || obj instanceof Long) {
                values.put(property.getColumnName(), (long) obj);
            } else if (type == float.class || obj instanceof Float) {
                values.put(property.getColumnName(), (float) obj);
            } else if (type == double.class || obj instanceof Double) {
                values.put(property.getColumnName(), (double) obj);
            } else if (type == byte[].class || obj instanceof byte[]) {
                values.put(property.getColumnName(), (byte[]) obj);
            }
        }
    }

    private static String getTableName(Class<? extends Model> clazz) {
        return HydrogenContext.getDefault().getTableName(clazz);
    }

    private static List<Property> getProperties(Class<? extends Model> clazz) {
        return HydrogenContext.getDefault().getProperties(clazz);
    }

    private static SQLiteDatabase getWritableDatabase() {
        return HydrogenContext.getDefault().getWritableDatabase();
    }

    private static SQLiteDatabase getReadableDatabase() {
        return HydrogenContext.getDefault().getReadableDatabase();
    }
}
